/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 *
 */
package org.apache.asyncweb.server.session;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.asyncweb.server.HttpSession;
import org.apache.asyncweb.server.util.LinkedPermitIssuer;
import org.apache.asyncweb.server.util.TimedPermit;
import org.apache.asyncweb.server.util.PermitExpirationListener;
import org.apache.asyncweb.server.util.TimedPermitIssuer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A simple <code>SessionStore</code> implementation which holds all session
 * data in memory. A <code>TimedPermitIssuer</code> is employed to issue a time-out
 * permit for each issued session.
 *
 * @author irvingd
 *
 */
public class BasicSessionStore implements HttpSessionStore {

    private static final Logger LOG = LoggerFactory
            .getLogger(BasicSessionStore.class);

    /**
     * Default session timeout of 15 minutes
     */
    private static final long DEFAULT_SESSION_TIMEOUT = 900000;

    private Map<String, HttpSession> sessionMap = Collections
            .synchronizedMap(new HashMap<String, HttpSession>());

    private List<HttpSessionListener> listeners = Collections
            .synchronizedList(new ArrayList<HttpSessionListener>());

    private TimedPermitIssuer permitIssuer;

    private boolean isClosed;

    /**
     * Constructs with the default session timeout
     */
    public BasicSessionStore() {
        this(DEFAULT_SESSION_TIMEOUT);
    }

    /**
     * Constructs with a specified session timeout
     *
     * @param sessionTimeout  The session timeout (in ms)
     */
    public BasicSessionStore(long sessionTimeout) {
        permitIssuer = new LinkedPermitIssuer(sessionTimeout);
        permitIssuer.addPermitExpirationListener(new TimeoutListener());
        LOG.debug("BasicSessionStore timeout: " + sessionTimeout + "ms");
    }

    /**
     * Adds a listener to this store
     *
     * @param listener The listener to add
     */
    public void addSessionListener(HttpSessionListener listener) {
        listeners.add(listener);
    }

    /**
     * Sets the listeners employed by this store.
     * Any existing listeners are removed
     *
     * @param listeners  The listeners to be added
     */
    public void setSessionListeners(Collection<HttpSessionListener> listeners) {
        synchronized (this.listeners) {
            this.listeners.clear();
            this.listeners.addAll(listeners);
        }
    }

    /**
     * Closes this store.
     * Our permit issuer is closed, and all sessions are destroyed.
     */
    public void close() {
        List<HttpSession> closureList = null;

        synchronized (sessionMap) {
            if (isClosed) {
                LOG.debug("Already closed");
                return;
            }
            LOG.debug("BasicSessionStore closing");
            permitIssuer.close();
            isClosed = true;
            closureList = new ArrayList<HttpSession>(sessionMap.values());
        }
        for ( HttpSession httpSession : closureList) {
         BasicSession session = (BasicSession) httpSession;
         LOG.debug("Closure: Destroying session: " + session.getId());
         session.destroy();
      }
    }

    /**
     * Creates a new session for the specified key.
     *
     * @param key  The session key
     * @return The created session, or <code>null</code> if a session is already
     *         held for the specified key
     */
    public HttpSession createSession(String key) {
        BasicSession created = null;
        synchronized (sessionMap) {
            if (isClosed) {
                throw new IllegalStateException("Store closed");
            }
            if (!sessionMap.containsKey(key)) {
                created = new BasicSession(key, this);
                sessionMap.put(key, created);
                TimedPermit permit = permitIssuer.issuePermit(created);
                created.setPermit(permit);
            }
        }
        if (created != null) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("New session created with key '" + key
                        + "'. Firing notifications");
            }
            fireCreated(created);
        }
        return created;
    }

    /**
     * Locates the session with the specified key.
     * If the session is found, we request it to renew its access permit.
     *
     * @param key  The key for which a session is required
     * @return     The located session, or <code>null</code> if no session with the
     *             specified key was found
     */
    public HttpSession locateSession(String key) {
        BasicSession session = (BasicSession) sessionMap.get(key);
        if (session != null) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Located session with key '" + key
                        + "'. Marking as accessed");
            }
            session.access();
        }
        return session;
    }

    /**
     * Invoked by a session we created when it successfully processes an expiry.
     * The session is removed from our session map, and expiry notifications are fired.
     *
     * @param session  The expired session
     */
    void sessionExpired(BasicSession session) {
        if (LOG.isDebugEnabled()) {
            LOG
                    .debug("Session has been expired. Processing notifications for '"
                            + session.getId() + "'");
        }
        sessionMap.remove(session.getId());
        fireExpiry(session);
    }

    /**
     * Invoked by a session we created when it successfully processes a destruction
     * request.
     * The session is removed from our session map, and destruction notifications
     * are fired
     *
     * @param session  The destroyed session
     */
    void sessionDestroyed(BasicSession session) {
        if (LOG.isDebugEnabled()) {
            LOG
                    .debug("Session has been destroyed. Processing notifications for '"
                            + session.getId() + "'");
        }
        sessionMap.remove(session.getId());
        fireDestroyed(session);
    }

    /**
     * Invoked when the permit associated with the specified session expires.
     * We simply request the session to expire itself.
     * If the session is not already destroyed, it will request us to fire
     * notifications on its behalf.
     *
     * @param session  The expired session
     */
    private void sessionPermitExpired(BasicSession session) {
        session.expire();
    }

    /**
     * Fires creation notification to all listeners assocaited with this store
     *
     * @param session  The expired session
     */
    private void fireCreated(HttpSession session) {
        synchronized (listeners) {
            for (HttpSessionListener listener : listeners) {
            listener.sessionCreated(session);
         }
        }
    }

    /**
     * Fires destruction notification to all listeners assocaited with this store
     *
     * @param session  The expired session
     */
    private void fireDestroyed(HttpSession session) {
        synchronized (listeners) {
            for (HttpSessionListener listener : listeners) {
            listener.sessionDestroyed(session);
         }
        }
    }

    /**
     * Fires expiry notification to all listeners assocaited with this store
     *
     * @param session  The expired session
     */
    private void fireExpiry(HttpSession session) {
        synchronized (listeners) {
            for (HttpSessionListener listener : listeners) {
            listener.sessionExpired(session);
         }
        }
    }

    /**
     * Receives notifications of timed out permits issued by this store,
     * and triggers expiry of the associated session
     *
     * @author irvingd
     *
     */
    private class TimeoutListener implements PermitExpirationListener
    {

        /**
         * Invoked when a permit issued for a session expires
         *
         * @param session  The session which has expired
         */
        public void permitExpired(Object session) {
            sessionPermitExpired((BasicSession) session);
        }

    }

}
